/* eslint-disable @typescript-eslint/no-unused-vars */
import { emptyArray } from '@aurelia/kernel';
import type { IVisitor } from './ast.visitor';

/** @internal */ export const ekAccessThis = 'AccessThis';
/** @internal */ export const ekAccessBoundary = 'AccessBoundary';
/** @internal */ export const ekAccessGlobal = 'AccessGlobal';
/** @internal */ export const ekAccessScope = 'AccessScope';
/** @internal */ export const ekArrayLiteral = 'ArrayLiteral';
/** @internal */ export const ekObjectLiteral = 'ObjectLiteral';
/** @internal */ export const ekPrimitiveLiteral = 'PrimitiveLiteral';
/** @internal */ export const ekNew = 'New';
/** @internal */ export const ekTemplate = 'Template';
/** @internal */ export const ekUnary = 'Unary';
/** @internal */ export const ekCallScope = 'CallScope';
/** @internal */ export const ekCallMember = 'CallMember';
/** @internal */ export const ekCallFunction = 'CallFunction';
/** @internal */ export const ekCallGlobal = 'CallGlobal';
/** @internal */ export const ekAccessMember = 'AccessMember';
/** @internal */ export const ekAccessKeyed = 'AccessKeyed';
/** @internal */ export const ekTaggedTemplate = 'TaggedTemplate';
/** @internal */ export const ekBinary = 'Binary';
/** @internal */ export const ekConditional = 'Conditional';
/** @internal */ export const ekAssign = 'Assign';
/** @internal */ export const ekArrowFunction = 'ArrowFunction';
/** @internal */ export const ekValueConverter = 'ValueConverter';
/** @internal */ export const ekBindingBehavior = 'BindingBehavior';
/** @internal */ export const ekArrayBindingPattern = 'ArrayBindingPattern';
/** @internal */ export const ekObjectBindingPattern = 'ObjectBindingPattern';
/** @internal */ export const ekBindingIdentifier = 'BindingIdentifier';
/** @internal */ export const ekForOfStatement = 'ForOfStatement';
/** @internal */ export const ekInterpolation = 'Interpolation';
/** @internal */ export const ekArrayDestructuring = 'ArrayDestructuring';
/** @internal */ export const ekObjectDestructuring = 'ObjectDestructuring';
/** @internal */ export const ekDestructuringAssignmentLeaf = 'DestructuringAssignmentLeaf';
/** @internal */ export const ekCustom = 'Custom';

export type ExpressionKind =
  | 'AccessThis'
  | 'AccessGlobal'
  | 'AccessBoundary'
  | 'AccessScope'
  | 'ArrayLiteral'
  | 'ObjectLiteral'
  | 'PrimitiveLiteral'
  | 'New'
  | 'Template'
  | 'Unary'
  | 'CallScope'
  | 'CallMember'
  | 'CallFunction'
  | 'CallGlobal'
  | 'AccessMember'
  | 'AccessKeyed'
  | 'TaggedTemplate'
  | 'Binary'
  | 'Conditional'
  | 'Assign'
  | 'ArrowFunction'
  | 'ValueConverter'
  | 'BindingBehavior'
  | 'ArrayBindingPattern'
  | 'ObjectBindingPattern'
  | 'BindingIdentifier'
  | 'ForOfStatement'
  | 'Interpolation'
  | 'ArrayDestructuring'
  | 'ObjectDestructuring'
  | 'DestructuringAssignmentLeaf'
  | 'Custom';

export type UnaryOperator = 'void' | 'typeof' | '!' | '-' | '+' | '++' | '--';
export type BinaryOperator = '??' | '&&' | '||' | '==' | '===' | '!=' | '!==' | 'instanceof' | 'in' | '+' | '-' | '*' | '/' | '%' | '**' | '<' | '>' | '<=' | '>=';
export type AssignmentOperator = '=' | '/=' | '*=' | '+=' | '-=';

export type IsPrimary = AccessThisExpression | AccessBoundaryExpression | AccessScopeExpression | AccessGlobalExpression | ArrayLiteralExpression | ObjectLiteralExpression | PrimitiveLiteralExpression | TemplateExpression | NewExpression;
export type IsLiteral = ArrayLiteralExpression | ObjectLiteralExpression | PrimitiveLiteralExpression | TemplateExpression;
export type IsLeftHandSide = IsPrimary | CallGlobalExpression | CallFunctionExpression | CallMemberExpression | CallScopeExpression | AccessMemberExpression | AccessKeyedExpression | TaggedTemplateExpression;
export type IsUnary = IsLeftHandSide | UnaryExpression;
export type IsBinary = IsUnary | BinaryExpression;
export type IsConditional = IsBinary | ConditionalExpression;
export type IsAssign = IsConditional | AssignExpression | ArrowFunction;
export type IsValueConverter = IsAssign | ValueConverterExpression;
export type IsBindingBehavior = IsValueConverter | BindingBehaviorExpression;
export type IsAssignable = AccessScopeExpression | AccessKeyedExpression | AccessMemberExpression | AssignExpression;
export type IsExpression = IsBindingBehavior | Interpolation;
export type BindingIdentifierOrPattern = BindingIdentifier | ArrayBindingPattern | ObjectBindingPattern;
export type IsExpressionOrStatement = IsExpression | ForOfStatement | BindingIdentifierOrPattern | DestructuringAssignmentExpression | DestructuringAssignmentSingleExpression | DestructuringAssignmentRestExpression;
export type AnyBindingExpression<TCustom extends CustomExpression = CustomExpression> = Interpolation | ForOfStatement | TCustom | IsBindingBehavior;

// Kept as a class because it carries runtime behavior methods (evaluate, assign, etc.)
export class CustomExpression {
  public readonly $kind = ekCustom;
  public constructor(public readonly value: unknown) {}
  public evaluate(...params: unknown[]): unknown { return this.value; }
  public assign(...params: unknown[]): unknown { return params; }
  public bind(...params: unknown[]): void { /* empty */ }
  public unbind(...params: unknown[]): void { /* empty */ }
  public accept<T>(_visitor: IVisitor<T>): T { return (void 0)!; }
}

export interface BindingBehaviorExpression {
  readonly $kind: 'BindingBehavior';
  readonly key: string;
  readonly expression: IsBindingBehavior;
  readonly name: string;
  readonly args: readonly IsAssign[];
}

export function createBindingBehaviorExpression(expression: IsBindingBehavior, name: string, args: readonly IsAssign[]): BindingBehaviorExpression {
  return { $kind: ekBindingBehavior, key: `_bb_${name}`, expression, name, args };
}

export interface ValueConverterExpression {
  readonly $kind: 'ValueConverter';
  readonly expression: IsValueConverter;
  readonly name: string;
  readonly args: readonly IsAssign[];
}

export function createValueConverterExpression(expression: IsValueConverter, name: string, args: readonly IsAssign[]): ValueConverterExpression {
  return { $kind: ekValueConverter, expression, name, args };
}

export interface AssignExpression {
  readonly $kind: 'Assign';
  readonly target: IsAssignable;
  readonly value: IsAssign;
  readonly op: AssignmentOperator;
}

export function createAssignExpression(target: IsAssignable, value: IsAssign, op: AssignmentOperator = '='): AssignExpression {
  return { $kind: ekAssign, target, value, op };
}

export interface ConditionalExpression {
  readonly $kind: 'Conditional';
  readonly condition: IsBinary;
  readonly yes: IsAssign;
  readonly no: IsAssign;
}

export function createConditionalExpression(condition: IsBinary, yes: IsAssign, no: IsAssign): ConditionalExpression {
  return { $kind: ekConditional, condition, yes, no };
}

export interface AccessGlobalExpression {
  readonly $kind: 'AccessGlobal';
  readonly name: string;
}

export function createAccessGlobalExpression(name: string): AccessGlobalExpression {
  return { $kind: ekAccessGlobal, name };
}

export interface AccessThisExpression {
  readonly $kind: 'AccessThis';
  readonly ancestor: number;
}

export function createAccessThisExpression(ancestor: number = 0): AccessThisExpression {
  return { $kind: ekAccessThis, ancestor };
}

export interface AccessBoundaryExpression {
  readonly $kind: 'AccessBoundary';
}

export const AccessBoundary: AccessBoundaryExpression = { $kind: ekAccessBoundary };

export function createAccessBoundaryExpression(): AccessBoundaryExpression {
  return AccessBoundary;
}

export interface AccessScopeExpression {
  readonly $kind: 'AccessScope';
  readonly name: string;
  readonly ancestor: number;
}

export function createAccessScopeExpression(name: string, ancestor: number = 0): AccessScopeExpression {
  return { $kind: ekAccessScope, name, ancestor };
}

export interface AccessMemberExpression {
  readonly $kind: 'AccessMember';
  readonly accessGlobal: boolean;
  readonly object: IsLeftHandSide;
  readonly name: string;
  readonly optional: boolean;
}

function isAccessGlobal(ast: IsLeftHandSide) {
  return ast.$kind === ekAccessGlobal || ((ast.$kind === ekAccessMember || ast.$kind === ekAccessKeyed) && ast.accessGlobal);
}

export function createAccessMemberExpression(object: IsLeftHandSide, name: string, optional: boolean = false): AccessMemberExpression {
  return { $kind: ekAccessMember, accessGlobal: isAccessGlobal(object), object, name, optional };
}

export interface AccessKeyedExpression {
  readonly $kind: 'AccessKeyed';
  readonly accessGlobal: boolean;
  readonly object: IsLeftHandSide;
  readonly key: IsAssign;
  readonly optional: boolean;
}

export function createAccessKeyedExpression(object: IsLeftHandSide, key: IsAssign, optional: boolean = false): AccessKeyedExpression {
  return { $kind: ekAccessKeyed, accessGlobal: isAccessGlobal(object), object, key, optional };
}

export interface NewExpression {
  readonly $kind: 'New';
  readonly func: IsLeftHandSide;
  readonly args: readonly IsAssign[];
}

export function createNewExpression(func: IsLeftHandSide, args: readonly IsAssign[]): NewExpression {
  return { $kind: ekNew, func, args };
}

export interface CallScopeExpression {
  readonly $kind: 'CallScope';
  readonly name: string;
  readonly args: readonly IsAssign[];
  readonly ancestor: number;
  readonly optional: boolean;
}

export function createCallScopeExpression(name: string, args: readonly IsAssign[], ancestor: number = 0, optional: boolean = false): CallScopeExpression {
  return { $kind: ekCallScope, name, args, ancestor, optional };
}

export interface CallMemberExpression {
  readonly $kind: 'CallMember';
  readonly object: IsLeftHandSide;
  readonly name: string;
  readonly args: readonly IsAssign[];
  readonly optionalMember: boolean;
  readonly optionalCall: boolean;
}

export function createCallMemberExpression(object: IsLeftHandSide, name: string, args: readonly IsAssign[], optionalMember: boolean = false, optionalCall: boolean = false): CallMemberExpression {
  return { $kind: ekCallMember, object, name, args, optionalMember, optionalCall };
}

export interface CallFunctionExpression {
  readonly $kind: 'CallFunction';
  readonly func: IsLeftHandSide;
  readonly args: readonly IsAssign[];
  readonly optional: boolean;
}

export function createCallFunctionExpression(func: IsLeftHandSide, args: readonly IsAssign[], optional: boolean = false): CallFunctionExpression {
  return { $kind: ekCallFunction, func, args, optional };
}

export interface CallGlobalExpression {
  readonly $kind: 'CallGlobal';
  readonly name: string;
  readonly args: readonly IsAssign[];
}

export function createCallGlobalExpression(name: string, args: readonly IsAssign[]): CallGlobalExpression {
  return { $kind: ekCallGlobal, name, args };
}

export interface BinaryExpression {
  readonly $kind: 'Binary';
  readonly operation: BinaryOperator;
  readonly left: IsBinary;
  readonly right: IsBinary;
}

export function createBinaryExpression(operation: BinaryOperator, left: IsBinary, right: IsBinary): BinaryExpression {
  return { $kind: ekBinary, operation, left, right };
}

export interface UnaryExpression {
  readonly $kind: 'Unary';
  readonly operation: UnaryOperator;
  readonly expression: IsLeftHandSide;
  readonly pos: 0 | 1;
}

export function createUnaryExpression(operation: UnaryOperator, expression: IsLeftHandSide, pos: 0 | 1 = 0): UnaryExpression {
  return { $kind: ekUnary, operation, expression, pos };
}

export interface PrimitiveLiteralExpression<TValue extends null | undefined | number | boolean | string = null | undefined | number | boolean | string> {
  readonly $kind: 'PrimitiveLiteral';
  readonly value: TValue;
}

export const PrimitiveLiteral = {
  $undefined: { $kind: ekPrimitiveLiteral, value: undefined } satisfies PrimitiveLiteralExpression<undefined>,
  $null: { $kind: ekPrimitiveLiteral, value: null } satisfies PrimitiveLiteralExpression<null>,
  $true: { $kind: ekPrimitiveLiteral, value: true } satisfies PrimitiveLiteralExpression<true>,
  $false: { $kind: ekPrimitiveLiteral, value: false } satisfies PrimitiveLiteralExpression<false>,
  $empty: { $kind: ekPrimitiveLiteral, value: '' } satisfies PrimitiveLiteralExpression<string>,
};

export function createPrimitiveLiteralExpression<T extends null | undefined | number | boolean | string>(value: T): PrimitiveLiteralExpression<T> {
  return { $kind: ekPrimitiveLiteral, value };
}

export interface ArrayLiteralExpression {
  readonly $kind: 'ArrayLiteral';
  readonly elements: readonly IsAssign[];
}

export const ArrayLiteral = {
  $empty: { $kind: ekArrayLiteral, elements: emptyArray } satisfies ArrayLiteralExpression
};

export function createArrayLiteralExpression(elements: readonly IsAssign[]): ArrayLiteralExpression {
  return { $kind: ekArrayLiteral, elements };
}

export interface ObjectLiteralExpression {
  readonly $kind: 'ObjectLiteral';
  readonly keys: readonly (number | string)[];
  readonly values: readonly IsAssign[];
}

export const ObjectLiteral = {
  $empty: { $kind: ekObjectLiteral, keys: emptyArray, values: emptyArray } satisfies ObjectLiteralExpression
};

export function createObjectLiteralExpression(keys: readonly (number | string)[], values: readonly IsAssign[]): ObjectLiteralExpression {
  return { $kind: ekObjectLiteral, keys, values };
}

export interface TemplateExpression {
  readonly $kind: 'Template';
  readonly cooked: readonly string[];
  readonly expressions: readonly IsAssign[];
}

export const Template = {
  $empty: { $kind: ekTemplate, cooked: [''], expressions: emptyArray } satisfies TemplateExpression
};

export function createTemplateExpression(cooked: readonly string[], expressions: readonly IsAssign[] = emptyArray): TemplateExpression {
  return { $kind: ekTemplate, cooked, expressions };
}

export interface TaggedTemplateExpression {
  readonly $kind: 'TaggedTemplate';
  readonly cooked: readonly string[] & { raw?: readonly string[] };
  readonly func: IsLeftHandSide;
  readonly expressions: readonly IsAssign[];
}

export function createTaggedTemplateExpression(cooked: readonly string[] & { raw?: readonly string[] }, raw: readonly string[], func: IsLeftHandSide, expressions: readonly IsAssign[] = emptyArray): TaggedTemplateExpression {
  cooked.raw = raw;
  return { $kind: ekTaggedTemplate, cooked, func, expressions };
}

export interface ArrayBindingPattern {
  readonly $kind: 'ArrayBindingPattern';
  readonly elements: readonly IsAssign[];
}

export function createArrayBindingPattern(elements: readonly IsAssign[]): ArrayBindingPattern {
  return { $kind: ekArrayBindingPattern, elements };
}

export interface ObjectBindingPattern {
  readonly $kind: 'ObjectBindingPattern';
  readonly keys: readonly (string | number)[];
  readonly values: readonly IsAssign[];
}

export function createObjectBindingPattern(keys: readonly (string | number)[], values: readonly IsAssign[]): ObjectBindingPattern {
  return { $kind: ekObjectBindingPattern, keys, values };
}

export interface BindingIdentifier {
  readonly $kind: 'BindingIdentifier';
  readonly name: string;
}

export function createBindingIdentifier(name: string): BindingIdentifier {
  return { $kind: ekBindingIdentifier, name };
}

export interface ForOfStatement {
  readonly $kind: 'ForOfStatement';
  readonly declaration: BindingIdentifierOrPattern | DestructuringAssignmentExpression;
  readonly iterable: IsBindingBehavior;
  readonly semiIdx: number;
}

export function createForOfStatement(declaration: BindingIdentifierOrPattern | DestructuringAssignmentExpression, iterable: IsBindingBehavior, semiIdx: number): ForOfStatement {
  return { $kind: ekForOfStatement, declaration, iterable, semiIdx };
}

export interface Interpolation {
  readonly $kind: 'Interpolation';
  readonly isMulti: boolean;
  readonly firstExpression: IsBindingBehavior;
  readonly parts: readonly string[];
  readonly expressions: readonly IsBindingBehavior[];
}

export function createInterpolation(parts: readonly string[], expressions: readonly IsBindingBehavior[] = emptyArray): Interpolation {
  return {
    $kind: ekInterpolation,
    isMulti: expressions.length > 1,
    firstExpression: expressions[0],
    parts,
    expressions
  };
}

export interface DestructuringAssignmentExpression {
  readonly $kind: 'ArrayDestructuring' | 'ObjectDestructuring';
  readonly list: readonly (DestructuringAssignmentExpression | DestructuringAssignmentSingleExpression | DestructuringAssignmentRestExpression)[];
  readonly source: AccessMemberExpression | AccessKeyedExpression | undefined;
  readonly initializer: IsBindingBehavior | undefined;
}

export function createDestructuringAssignmentExpression($kind: 'ArrayDestructuring' | 'ObjectDestructuring', list: readonly (DestructuringAssignmentExpression | DestructuringAssignmentSingleExpression | DestructuringAssignmentRestExpression)[], source: AccessMemberExpression | AccessKeyedExpression | undefined, initializer: IsBindingBehavior | undefined): DestructuringAssignmentExpression {
  return { $kind, list, source, initializer };
}

export interface DestructuringAssignmentSingleExpression {
  readonly $kind: 'DestructuringAssignmentLeaf';
  readonly target: AccessMemberExpression;
  readonly source: AccessMemberExpression | AccessKeyedExpression;
  readonly initializer: IsBindingBehavior | undefined;
}

export function createDestructuringAssignmentSingleExpression(target: AccessMemberExpression, source: AccessMemberExpression | AccessKeyedExpression, initializer: IsBindingBehavior | undefined): DestructuringAssignmentSingleExpression {
  return { $kind: ekDestructuringAssignmentLeaf, target, source, initializer };
}

export interface DestructuringAssignmentRestExpression {
  readonly $kind: 'DestructuringAssignmentLeaf';
  readonly target: AccessMemberExpression;
  readonly indexOrProperties: string[] | number;
}

export function createDestructuringAssignmentRestExpression(target: AccessMemberExpression, indexOrProperties: string[] | number): DestructuringAssignmentRestExpression {
  return { $kind: ekDestructuringAssignmentLeaf, target, indexOrProperties };
}

export interface ArrowFunction {
  readonly $kind: 'ArrowFunction';
  readonly args: BindingIdentifier[];
  readonly body: IsAssign;
  readonly rest: boolean;
}

export function createArrowFunction(args: BindingIdentifier[], body: IsAssign, rest: boolean = false): ArrowFunction {
  return { $kind: ekArrowFunction, args, body, rest };
}
